正如在前一节的示例，编程语言由许多元素组成，如关键字、标识符、数字、操作符等。词法分析器的任务是获取文本输入并从中创建一个标记序列。calc语言由，:，+，-，*，/，(，)和正则表达式([a- za -z])+(标识符)和([0-9])+(数字)组成。需要为每个标记分配唯一编号，以使标记更容易处理。

\mySubsubsection{2.3.1.}{手写词法分析器}

词法分析器的实现通常称为Lexer。这里，创建一个名为Lexer.h的头文件，并开始定义Token:

\begin{cpp}
#ifndef LEXER_H
#define LEXER_H

#include "llvm/ADT/StringRef.h"
#include "llvm/Support/MemoryBuffer.h"
\end{cpp}

llvm::MemoryBuffer类提供对内存块的只读访问，内存块由文件的内容填充，在缓冲区的末尾添加一个尾零字符('\verb|\|x00')。我们使用这个特性来读取缓冲区，而不必在每次访问时检查缓冲区的长度。StringRef类封装了一个指向C字符串及其长度的指针。因为对长度进行了保存，所以字符串不需要像普通的C字符串那样以零字符('\verb|\|x00')结束。StringRef实例允许指向由MemoryBuffer管理的内存。

了解了这一点后，我们开始实现Lexer类:

\begin{enumerate}
\item
首先，Token类包含前面提到的唯一标记编号的枚举定义:

\begin{cpp}
class Lexer;

class Token {
    friend class Lexer;

public:
    enum TokenKind : unsigned short {
        eoi, unknown, ident, number, comma, colon, plus,
        minus, star, slash, l_paren, r_paren, KW_with
    };
\end{cpp}

除了为每个标记定义成员外，还添加了两个值:eoi和unknown。eoi表示输入结束，当处理完输入的所有字符时返回。unknown在词法级别发生错误时使用，例如，\#不是该语言的标记，将映射为unknown。

\item
除了枚举之外，该类还有一个Text成员，该成员指向标记文本的开头，使用了前面提到的StringRef类:

\begin{cpp}
private:
    TokenKind Kind;
    llvm::StringRef Text;

public:
    TokenKind getKind() const { return Kind; }
    llvm::StringRef getText() const { return Text; }
\end{cpp}

这对于语义处理很有意义，例如：对于标识符，了解名称很有用。

\item
is()和isOneOf()方法用于测试标识是否属于某种类型。isOneOf()方法使用可变的模板，允许可变数量的参数:

\begin{cpp}
    bool is(TokenKind K) const { return Kind == K; }
    bool isOneOf(TokenKind K1, TokenKind K2) const {
        return is(K1) || is(K2);
    }
    template <typename... Ts>
    bool isOneOf(TokenKind K1, TokenKind K2, Ts... Ks) const {
        return is(K1) || isOneOf(K2, Ks...);
    }
};
\end{cpp}

\item
头文件中，Lexer类本身也有类似的接口:

\begin{cpp}
class Lexer {
    const char *BufferStart;
    const char *BufferPtr;

public:
    Lexer(const llvm::StringRef &Buffer) {
        BufferStart = Buffer.begin();
        BufferPtr = BufferStart;
    }

    void next(Token &token);

private:
    void formToken(Token &Result, const char *TokEnd,
                   Token::TokenKind Kind);
};
#endif
\end{cpp}

除了构造函数之外，公共接口只有next()方法，该方法返回下一个标识。该方法的作用类似于迭代器，总是前进到下一个可用标识。该类的唯一成员是指向输入开头和下一个未处理字符的指针，假设缓冲区以0结束(就像C字符串一样)。

\item
在Lexer.cpp文件中实现Lexer类，从一些帮助函数开始分类字符:

\begin{cpp}
#include "Lexer.h"
namespace charinfo {
    LLVM_READNONE inline bool isWhitespace(char c) {
        return c == ' ' || c == '\t' || c == '\f' ||
        c == '\v' ||
        c == '\r' || c == '\n';
    }
    LLVM_READNONE inline bool isDigit(char c) {
        return c >= '0' && c <= '9';
    }
    LLVM_READNONE inline bool isLetter(char c) {
        return (c >= 'a' && c <= 'z') ||
        (c >= 'A' && c <= 'Z');
}
}
\end{cpp}

这些函数使条件更具有可读性。

\begin{myNotic}{Note}
没有使用<cctype>标准库头文件提供的函数有两个原因。首先，这些函数根据环境中定义的语言环境改变行为。例如，区域设置是德语区域，那么德语的变音符可以分类为字母。这在编译器中通常是不需要的。其次，由于函数的形参类型为int，因此需要从char类型进行转换。这种转换的结果取决于char是视为有符号类型还是无符号类型，这会导致移植性问题。
\end{myNotic}

\item
前一节对语法的了解中，知道了该语言的所有符号，但语法没有定义应该忽略的字符。例如，空格或换行字符只添加空白，通常应该忽略。next()一开始就会忽略这些字符:

\begin{cpp}
void Lexer::next(Token &token) {
    while (*BufferPtr &&
    charinfo::isWhitespace(*BufferPtr)) {
        ++BufferPtr;
    }
\end{cpp}

\item
接下来，确保仍然有字符需要处理:

\begin{cpp}
    if (!*BufferPtr) {
        token.Kind = Token::eoi;
        return;
    }
\end{cpp}

至少有一个字符需要处理。

\item
首先检查字符是小写还是大写。所以，标记要么是标识符，要么是with关键字，因为标识符的正则表达式也匹配关键字。最常见的解决方案是，收集正则表达式匹配的字符，并检查字符串是否恰好是关键字:

\begin{cpp}
    if (charinfo::isLetter(*BufferPtr)) {
        const char *end = BufferPtr + 1;
        while (charinfo::isLetter(*end))
            ++end;
        llvm::StringRef Name(BufferPtr, end - BufferPtr);
        Token::TokenKind kind =
            Name == "with" ? Token::KW_with : Token::ident;
        formToken(token, end, kind);
        return;
    }
\end{cpp}

formToken()私有方法用于填充标记。

\item
接下来，检查数字。与前面的代码片段非常相似:

\begin{cpp}
    else if (charinfo::isDigit(*BufferPtr)) {
        const char *end = BufferPtr + 1;
        while (charinfo::isDigit(*end))
            ++end;
        formToken(token, end, Token::number);
        return;
    }
\end{cpp}

现在只剩下由固定字符串定义的标记。

\item
这很容易用switch完成。由于所有这些标记都只有一个字符，因此使用CASE预处理器宏来减少编码量:

\begin{cpp}
else {
    switch (*BufferPtr) {
        #define CASE(ch, tok) \
        case ch: formToken(token, BufferPtr + 1, tok); break
        CASE('+', Token::plus);
        CASE('-', Token::minus);
        CASE('*', Token::star);
        CASE('/', Token::slash);
        CASE('(', Token::Token::l_paren);
        CASE(')', Token::Token::r_paren);
        CASE(':', Token::Token::colon);
        CASE(',', Token::Token::comma);
        #undef CASE
\end{cpp}

\item
最后，需要检查不支持的字符:

\begin{cpp}
        default:
            formToken(token, BufferPtr + 1, Token::unknown);
        }
        return;
    }
}
\end{cpp}

只有formToken()私有辅助方法仍然缺失。

\item
填充Token实例的成员，并更新指向下一个未处理字符的指针:

\begin{cpp}
void Lexer::formToken(Token &Tok, const char *TokEnd,
                      Token::TokenKind Kind) {
    Tok.Kind = Kind;
    Tok.Text = llvm::StringRef(BufferPtr,
                               TokEnd - BufferPtr);
    BufferPtr = TokEnd;
}
\end{cpp}

\end{enumerate}

下一节中，我们将了解如何构造用于语法分析的解析器。






